跳到主要内容

MySQL 中的文件与日志

参数文件

当 MySQL 实例启动时,数据库会先去读一个配置参数文件,用来寻找数据库的各种文件所在位置以及指定某些初始化参数,这些参数通常定义了某种内存结构有多大等。

在默认情况下,MySQL实例会按照一定的顺序在指定的位置进行读取,用户只需通过命令 mysql--helpI grep my.cnf 来寻找即可。

日志文件

日志文件记录了影响 MySQL 数据库的各种类型活动。MySQL 数据库中常见的日志文件有:

  • 错误日志(error log)
  • 二进制日志(binlog)
  • 慢查询日志(slow query log)
  • 查询日志(log)

这些日志又件可以希助 DBA 对 MySQL 数据库的运行状念进行诊断,从向更好地进行数据库层面的优化。

表结构定义文件 frm

因为 MySQL 插件式存储引擎的体系结构的关系,MySQL 数据的存储是根据表进行的,每个表都会有与之对应的文件。但不论表采用何种存储引擎,MySQL 都有一个以 frm 为后缀名的文件,这个文件记录了该表的表结构定义。

frm 还用来存放视图的定义,如用户创建了一个 v_a 视图,那么对应地会产生一个 v_a.frm 文件,用来记录视图的定义,该文件是文本文件,可以直接使用 cat 命令进行查看(但是一般有编码问题,导致乱码):

错误日志文件

错误日志文件对 MySQL 的启动、运行、关闭过程进行了记录。MySQL DBA 在遇到问题时应该首先查看该文件以便定位问题。该文件不仅记录了所有的错误信息,也记录一些警告信息或正确的信息。用户可以通过命令来定位该文件,如:

SHOW VARIABLES LIKE 'log_error';

当数据库不能重启时,就可以查错误日志文件

tail -n 50 /var/log/mysqld.log

慢查询日志(slow query log)

慢查询日志(slowlog)可帮助 DBA 定位可能存在问题的 SQL 语句,从而进行 SQL 语句层面的优化。

例如,可以在 MySQL 启动时设一个阈值,将运行时间超过该值的所有 SQL 语句都记录到慢查询日志文件中。DBA 每天或每过一段时间对其进行检查,确认是否有 SQL 语句需要进行优化。

该阈值可以通过参数 long_query_time 来设置,默认值为 10,代表 10 秒。

SHOW VARIABLES LIKE 'long_query_time';

在默认情况下,MySQL 数据库并不启动慢查询日志,用户需要手工将这个参数设为 ON

-- 旧版是这个
SHOW VARIABLES LIKE 'log_slow_queries';

-- 新版用这个
show variables like 'slow_query%';

参数说明:

  • slow_query_log 慢查询开启状态,ON开启,OFF关闭
  • slow_query_log_file 慢查询日志存放的位置(这个目录需要 MySQL 的运行帐号的可写权限,一般设置为 MySQL 的数据存放目录)
  • long_query_time 查询超过多少秒才记录

启用:

set global slow_query_log='ON';

方法二:配置文件设置(服务器重启不影响)

修改配置文件 my.cnf,在 [mysqld] 下的下方加入

[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log //linux
long_query_time = 1

查询日志(general log)

记录了服务器接收到的每一个查询或是命令,无论这些查询或是命令是否正确甚至是否包含语法错误,general log 都会将其记录下来 ,记录的格式为 {Time ,Id ,Command,Argument }

也正因为 mysql 服务器需要不断地记录日志,开启 General log 会产生不小的系统开销。 因此,Mysql默认是把 General log 关闭的。

查看日志的存放方式(表还是文件):

show variables like 'log_output';

默认是文件

如果设置

set global log_output=table 

则日志结果会记录到名为 gengera_log 的表中,这表的默认引擎都是 CSV 如果设置表数据到文件

set global log_output=file;

设置 general log 的日志文件路径:

set global general_log_file='/tmp/general.log';
-- 开启general log:
set global general_log=on;
-- 关闭general log:
set global general_log=off;

一条更新语句是如何执行的?

这里直接复用前面体系结构学习那一块的段落,来复习下 redo log 和 binlog 的作用

更新语句如何执行的呢?

sql 语句如下:

update tb_student A set A.age='19' where A.name='张三';

来给张三修改下年龄,其实条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候要记录日志,这就会引入日志模块了,MySQL 自带的日志模块式 binlog(归档日志) ,所有的存储引擎都可以使用,常用的 InnoDB 引擎还自带了一个日志模块 redolog(重做日志),就以 InnoDB 模式下来探讨这个语句的执行流程。

流程如下

1、先查询到张三这一条数据,如果有缓存,也是会用到缓存。

2、然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。

3、执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。

4、更新完成。

重要的日志模块:redo log

不知道你还记不记得《孔乙己》这篇文章,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本

如果有人要赊账或者还账的话,掌柜一般有两种做法:

  • 一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
  • 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。

在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。

这整个过程想想都麻烦。相比之下,还是先在粉板上记一下方便。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?

同样,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问 题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。

而粉板和账本配合的整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。

具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。

如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。

与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。 checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文 件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。

重要的日志模块:binlog

前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

为什么要用两个日志模块,用一个日志模块不行吗?

这是因为最开始 MySQL 并没有 InnoDB 引擎 ,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统,也就是 redo log 来实现 crash-safe 能力。

但是 redolog 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力,binlog 日志只能用来归档。

两种日志的区别

这两种日志有以下三点不同。

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

  2. redo log 是物理日志,记录的是 “在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如 “给 ID = 2 这一行的 c 字段加 1”。

  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写” 是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

再看 update 语句时的内部流程

sql 语句如下:

update tb_student A set A.age='19' where A.name='张三';
  1. 执行器先找引擎取 A.name='张三' 这一行。name 是主键,引擎直接用树搜索找到这一行。如果 A.name='张三' 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

  2. 执行器拿到引擎给的行数据,把这个值设置为 19,比如原来是 N,现在就是 19,得到新的一行数据,再调用引擎接口写入这行新数据。

  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。

  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

给出这个 update 语句的执行流程图,深色框表示是在执行器中执行的。

graph TD A[取 name='张三' 这一行] --> B{数据页在内存中 ?}; B -- 是 --> C[返回行数据]; C --> D[将这行设置为 19]; D --> E[写入新行]; E --> F[新行更新到内存]; F --> G[写入 redo log, 其处于 prepare 阶段]; G --> H[写入 binlog]; H --> I[提交事务]; I --> J[redo log 状态改为 commit 状态]; B -- 否则 --> K[从磁盘读入内存]; K --> L[返回给执行器]; L --> D; classDef green fill:#008000,stroke:#333,stroke-width:2px; class A,D,E,H green;

你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

两阶段提交是什么?

为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。

因为如果先写 redo log 直接提交,然后写 binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。

如果先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。

而采用 redo log 两阶段提交的方式就不一样了,写完 binlog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binlog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:

判断 redo log 是否完整,如果判断是完整的,就立即提交。 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。

这样就解决了数据一致性的问题。

重做日志(redo log)

作用与内容

确保事务的持久性。redo 日志记录事务执行后的状态,用来恢复未写入 data file 的已成功事务更新的数据。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性。

物理格式的日志,记录的是物理数据页面的修改的信息,其 redo log 是顺序写入 redo log file 的物理文件中去的。

什么时候产生:

事务开始之后就产生 redo log,redo log 的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo log 文件中。(所以再大的事务 commit 的时候都很快)

什么时候释放:

当对应事务的脏页写入到磁盘之后,redo log 的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。

对应的物理文件:

默认情况下,对应的物理文件位于数据库的 data 目录下的 ib_logfile1 & ib_logfile2

配置参数:

  • innodb_log_group_home_dir 指定日志文件组所在的路径,默认 ./ ,表示在数据库的数据目录下。
  • innodb_log_files_in_group 指定重做日志文件组中文件的数量,默认 2

关于文件的大小和数量,由以下两个参数配置:

  • innodb_log_file_size 重做日志文件的大小。
  • innodb_mirrored_log_groups 指定了日志镜像文件组的数量,默认 1

什么时候写盘的?

很重要一点,redo log 是什么时候写盘的?前面说了是在事物开始之后逐步写盘的。之所以说重做日志是在事务开始之后逐步写入重做日志文件,而不一定是事务提交才写入重做日志缓存,原因就是,重做日志有一个缓存区 Innodb_log_bufferInnodb_log_buffer 的默认大小为 8M(这里设置的 16M),Innodb 存储引擎先将重做日志写入 innodb_log_buffer 中。

然后会通过以下三种方式将 innodb 日志缓冲区的日志刷新到磁盘

  • Master Thread 每秒一次执行刷新 Innodb_log_buffer 到重做日志文件。
  • 每个事务提交时会将重做日志刷新到重做日志文件。
  • 当重做日志缓存可用空间 少于一半时,重做日志缓存被刷新到重做日志文件

由此可以看出,重做日志通过不止一种方式写入到磁盘,尤其是对于第一种方式,Innodb_log_buffer 到重做日志文件是 Master Thread 线程的定时任务。

因此重做日志的写盘,并不一定是随着事务的提交才写入重做日志文件的,而是随着事务的开始,逐步开始的。

即使某个事务还没有提交,Innodb 存储引擎仍然每秒会将重做日志缓存刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释再大的事务的提交(commit)的时间也是很短暂的。

二进制日志(binlog)

作用与内容:

  • 用于复制,在主从复制中,从库利用主库上的 binlog 进行重播,实现主从同步。
  • 用于数据库的基于时间点的还原。

逻辑格式的日志,可以简单认为就是执行过的事务中的 sql 语句。但又不完全是 sql 语句这么简单,而是包括了执行的 sql 语句(增删改)反向的信息,也就意味着

  • delete 对应着 delete 本身和其反向的 insert;
  • update 对应着 update 执行前后的版本的信息;
  • insert 对应着 delete 和 insert 本身的信息。

所以可以基于 binlog 做到类似于 oracle 的闪回功能,其实都是依赖于 binlog 中的日志记录。

什么时候产生:

事务提交的时候,一次性将事务中的 sql 语句(一个事物可能对应多个 sql 语句)按照一定的格式记录到 binlog 中。这里与 redo log 很明显的差异就是 redo log 并不一定是在事务提交的时候刷新到磁盘,redo log 是在事务开始之后就开始逐步写入磁盘。

因此对于事务的提交,即便是较大的事务,提交(commit)都是很快的,但是在开启了 bin_log 的情况下,对于较大事务的提交,可能会变得比较慢一些。

这是因为 binlog 是在事务提交的时候一次性写入的造成的,这些可以通过测试验证。

什么时候释放:

binlog 的默认是保持时间由参数 expire_logs_days 配置,也就是说对于非活动的日志文件,在生成时间超过 expire_logs_days 配置的天数之后,会被自动删除。

回滚日志(undo log)

作用与内容

保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读

逻辑格式的日志,在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于 redo log 的。

什么时候产生和释放的?

事务开始之前,将当前是的版本生成 undo log,undo 也会产生 redo 来保证 undo log 的可靠性

当事务提交之后,undo log 并不能立马被删除,而是放入待清理的链表,由 purge 线程判断是否由其他事务在使用 undo 段中表的上一个事务之前的版本信息,决定是否可以清理 undo log 的日志空间。

对应的物理文件:

MySQL5.6之前,undo 表空间位于共享表空间的回滚段中,共享表空间的默认的名称是 ibdata,位于数据文件目录中。 MySQL5.6之后,undo 表空间可以配置成独立的文件,但是提前需要在配置文件中配置,完成数据库初始化后生效且不可改变 undo log 文件的个数

如果初始化数据库之前没有进行相关配置,那么就无法配置成独立的表空间了。

undo 是在事务开始之前保存的被修改数据的一个版本,产生 undo 日志的时候,同样会伴随类似于保护事务持久化机制的 redolog 的产生。

默认情况下 undo 文件是保持在共享表空间的,也即 ibdatafile 文件中,当数据库中发生一些大的事务性操作的时候,要生成大量的 undo 信息,全部保存在共享表空间中的。因此共享表空间可能会变的很大,默认情况下,也就是 undo 日志使用共享表空间的时候,被 “撑大” 的共享表空间是不会也不能自动收缩的。因此,mysql5.7 之后的 “独立 undo 表空间” 的配置就显得很有必要了。

References